<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use ReflectionClass, Closure;

/**
 * An abstract target for injection
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 *
 */
class InjectableContainer {

    public $instance = null;
    public $id = null;

    public function __construct($instance, $id) {
        $this->instance = $instance;
        $this->id = $id;
    }

}

/**
 * Classes implementing this interface can be injected with {@link \mg\Injection Injections}
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
interface Injectable {

    public function inject();

}

abstract class InjectableAdapter implements Injectable {

    public static function resolveInstance() {
        $className = get_called_class();

        return in(function(InjectableProvider $provider) use ($className) {
            return $provider->createInjectableInstance($className);
        });
    }
}
/**
 * Classes implementing this interface should be able to construct an \mg\Injectable
 * with proper injection support.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
interface InjectableProvider {

    /**
     * @param string $className
     *
     * @return \mg\Injectable
     */
    public function createInjectableInstance($className);

}

/**
 * The DefaultInjectableProvider creates an Injectable instance by creating InjectableContainers for
 * the to-be Injectable instance and binding it through an InjectionBinder
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 * @see \mg\InjectableContainerProvider
 * @see \mg\InjectionBinder
 */
class DefaultInjectableProvider implements InjectableProvider {

    /**
     *
     * @var InjectableContainerProvider
     */
    private $provider = null;

    /**
     *
     * @var InjectionBinder
     */
    private $binder = null;

    private $injectables = null;

    public function __construct(InjectableContainerProvider $provider, InjectionBinder $binder) {
        $this->provider = $provider;
        $this->binder = $binder;
        $this->injectables = array();
    }

    public function createInjectableInstance($className) {
        $className = mb_strtolower($className);

        if($className[0] === '\\') {
            $className = mb_substr($className, 1);
        }

        if(isset($this->injectables[$className])) {
            return $this->injectables[$className];
        }

        $injectableContainer = $this->provider->createInjectableContainer($className);

        $this->binder->bindInjections($injectableContainer, function(Injectable $instance) {
            $instance->inject();
        });

        $result = $injectableContainer->instance;
        $this->injectables[$className] = $result;

        return $result;
    }
}

/**
 * Classes implementing this interface should be able
 * to create {@link \mg\InjectableContainer InjectableContainers}
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 *
 */
interface InjectableContainerProvider {

    /**
     *
     * @param string $className
     * @param string $id
     *
     * @return \mg\InjectableContainer
     */
    public function createInjectableContainer($className, $id = null);

}

/**
 * The DefaultInjectableContainerProvider maps the required instance to a facade class of the
 * required instance with an empty constructor.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
class DefaultInjectableContainerProvider implements InjectableContainerProvider {

    public function createInjectableContainer($className, $id = null) {
        $reflectionClass = new \ReflectionClass($className);


        $shortName = $reflectionClass->getShortName();
        $oldNamespace = $reflectionClass->getNamespaceName();

        $namespace = trim('__generated__\\' . $oldNamespace, '\\');

        if(!class_exists($namespace . '\\' . $shortName, false)) {
            $body = "namespace {$namespace} { class $shortName extends \\{$className} { public function __construct() { } } }";
            eval($body);
        }

        $generatedClass = $namespace . '\\' . $shortName;

        $instance = new $generatedClass();

        return new InjectableContainer($instance, $id);
    }

}

/**
 * The InjectionBinder interface binds, keeps track of and unbinds {@link \mg\InjectableContainer InjectableContainers}.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
interface InjectionBinder {

    /**
     * Bind an InjectableContainer against a Closure
     *
     * @param Injectable $injectable
     * @param Closure $injector
     *
     */
    public function bindInjections(InjectableContainer $injectableContainer, Closure $injectorClosure);

    /**
     * Invoke unbind on all bound {@link \mg\Injection Injections}
     */
    public function unbindAll();
}

/**
 * The DefaultInjectionBinder works in cojunction with the DefaultInjectableContainerProvider, having
 * a dependency to facade instances of Injectable classes. The idea behind the facade class is that
 * first a class can be instantiated without constructor interference, next the inject method gets
 * called and finally the original constructor is called. This allows classes to depend on Injectable
 * behaviour from within the constructor.
 *
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 * @version 1.0
 */
class DefaultInjectionBinder implements InjectionBinder {

    private $injections = null;

    public function __construct() {
        $this->injections = array();
    }

    /**
     * {@inheritDoc}
     */
    public function bindInjections(InjectableContainer $injectableContainer, Closure $injectorClosure) {
        $instance = $injectableContainer->instance;

        if(mb_substr(\get_class($instance), 0, 14) !== '__generated__\\') {
            throw new InjectionException('Cannot bind injections to non-injectable class');
        }

        $reflectionClass = new \ReflectionClass($instance);

        // Inject the variables
        $injectorClosure($instance, $injectableContainer->id);

        /* @var $property ReflectionProperty */
        foreach($reflectionClass->getProperties() as $property) {
            if(!$property->isPublic() || $property->isStatic()) {
                continue;
            }

            $value = $property->getValue($instance);

            if(!($value instanceof Injection)) {
                continue;
            }

            $this->injections[] = $value;

            $value->bind($instance, mb_strtolower(mb_substr(\get_class($instance), 14)), ($injectableContainer->id === null ? '' : $injectableContainer->id), $property->getName());

        }

        $constructor = $reflectionClass->getParentClass()->getConstructor();

        if($constructor !== null) {
            // TODO : resolve efficiency issue here
            $context = Context :: getContext();
            $constructor->invokeArgs($instance, $context->injectParameterValues($constructor));
        }

        return $instance;

    }

    /**
     * {@inheritDoc}
     */
    public function unbindAll() {
        /* @var $injection Injection */
        foreach($this->injections as $index => $injection) {
            $injection->unbind();
            unset($this->injections[$index]);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function __destruct() {
        $this->unbindAll();
    }
}